附录D. 可执行jar格式

spring-boot-loader模块允许Spring Boot对可执行jar和war文件的支持。如果你正在使用Maven或Gradle插件,可执行jar会被自动产生,通常你不需要了解它是如何工作的。

如果你需要从一个不同的构建系统创建可执行jars,或你只是对底层技术好奇,本章节将提供一些背景资料。

附录D.1. 内嵌JARs

Java没有提供任何标准的方式来加载内嵌的jar文件(也就是jar文件自身包含到一个jar中)。如果你正分发一个在不解压缩的情况下可以从命令行运行的自包含应用,那这将是个问题。

为了解决这个问题,很多开发者使用"影子" jars。一个影子jar只是简单的将所有jars的类打包进一个单独的"超级jar"。使用影子jars的问题是它很难分辨在你的应用中实际可以使用的库。在多个jars中存在相同的文件名(内容不同)也是一个问题。Spring Boot另劈稀径,让你能够直接嵌套jars。

附录D.1.2. 可执行war文件结构

Spring Boot Loader兼容的war文件应该遵循以下结构:

example.jar
 |
 +-META-INF
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader
 |           +-<spring boot loader classes>
 +-WEB-INF
    +-classes
    |  +-com
    |     +-mycompany
    |        +-project
    |           +-YouClasses.class
    +-lib
    |  +-dependency1.jar
    |  +-dependency2.jar
    +-lib-provided
       +-servlet-api.jar
       +-dependency3.jar

依赖需要放到内嵌的WEB-INF/lib目录下。任何运行时需要但部署到普通web容器不需要的依赖应该放到WEB-INF/lib-provided目录下。

附录D.2. Spring Boot的"JarFile"类

Spring Boot用于支持加载内嵌jars的核心类是org.springframework.boot.loader.jar.JarFile。它允许你从一个标准的jar文件或内嵌的子jar数据中加载jar内容。当首次加载的时候,每个JarEntry的位置被映射到一个偏移于外部jar的物理文件:

myapp.jar
+---------+---------------------+
|         | /lib/mylib.jar      |
| A.class |+---------+---------+|
|         || B.class | B.class ||
|         |+---------+---------+|
+---------+---------------------+
^          ^          ^
0063       3452       3980

上面的示例展示了如何在myapp.jar的0063处找到A.class。来自于内嵌jar的B.class实际可以在myapp.jar的3452处找到,B.class可以在3980处找到(图有问题?)。

有了这些信息,我们就可以通过简单的寻找外部jar的合适部分来加载指定的内嵌实体。我们不需要解压存档,也不需要将所有实体读取到内存中。

附录D.2.1 对标准Java "JarFile"的兼容性

Spring Boot Loader努力保持对已有代码和库的兼容。org.springframework.boot.loader.jar.JarFile继承自java.util.jar.JarFile,可以作为降级替换。

附录D.3. 启动可执行jars

org.springframework.boot.loader.Launcher类是个特殊的启动类,用于一个可执行jars的主要入口。它实际上就是你jar文件的Main-Class,并用来设置一个合适的URLClassLoader,最后调用你的main()方法。

这里有3个启动器子类,JarLauncher,WarLauncher和PropertiesLauncher。它们的作用是从嵌套的jar或war文件目录中(相对于显示的从classpath)加载资源(.class文件等)。在[Jar|War]Launcher情况下,嵌套路径是固定的(lib/*.jar和war的lib-provided/*.jar),所以如果你需要很多其他jars只需添加到那些位置即可。PropertiesLauncher默认查找你应用存档的lib/目录,但你可以通过设置环境变量LOADER_PATH或application.properties中的loader.path来添加其他的位置(逗号分割的目录或存档列表)。

附录D.3.1 Launcher manifest

你需要指定一个合适的Launcher作为META-INF/MANIFEST.MFMain-Class属性。你实际想要启动的类(也就是你编写的包含main方法的类)需要在Start-Class属性中定义。

例如,这里有个典型的可执行jar文件的MANIFEST.MF:

Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.mycompany.project.MyApplication

对于一个war文件,它可能是这样的:

Main-Class: org.springframework.boot.loader.WarLauncher
Start-Class: com.mycompany.project.MyApplication

:你不需要在manifest文件中指定Class-Path实体,classpath会从嵌套的jars中被推导出来。

附录D.3.2. 暴露的存档

一些PaaS实现可能选择在运行前先解压存档。例如,Cloud Foundry就是这样操作的。你可以运行一个解压的存档,只需简单的启动合适的启动器:

$ unzip -q myapp.jar
$ java org.springframework.boot.loader.JarLaunche

附录D.4. PropertiesLauncher特性

PropertiesLauncher有一些特殊的性质,它们可以通过外部属性来启用(系统属性,环境变量,manifest实体或application.properties)。

Key 作用
loader.path 逗号分割的classpath,比如lib:${HOME}/app/lib
loader.home 其他属性文件的位置,比如/opt/app(默认为${user.dir}
loader.args main方法的默认参数(以空格分割)
loader.main 要启动的main类名称,比如com.app.Application
loader.config.name 属性文件名,比如loader(默认为application)
loader.config.location 属性文件路径,比如classpath:loader.properties(默认为application.properties)
loader.system 布尔标识,表明所有的属性都应该添加到系统属性中(默认为false)

Manifest实体keys通过大写单词首字母及将分隔符从"."改为"-"(比如Loader-Path)来进行格式化。loader.main是个特例,它是通过查找manifest的Start-Class,这样也兼容JarLauncher。

环境变量可以大写字母并且用下划线代替句号。

  • loader.home是其他属性文件(覆盖默认)的目录位置,只要没有指定loader.config.location
  • loader.path可以包含目录(递归地扫描jar和zip文件),存档路径或通配符模式(针对默认的JVM行为)。
  • 占位符在使用前会被系统和环境变量加上属性文件本身的值替换掉。

附录D.5. 可执行jar的限制

当使用Spring Boot Loader打包的应用时有一些你需要考虑的限制。

附录D.5.1 Zip实体压缩

对于一个嵌套jar的ZipEntry必须使用ZipEntry.STORED方法保存。这是需要的,这样我们可以直接查找嵌套jar中的个别内容。嵌套jar的内容本身可以仍旧被压缩,正如外部jar的其他任何实体。

附录D.5.2. 系统ClassLoader

启动的应用在加载类时应该使用Thread.getContextClassLoader()(多数库和框架都默认这样做)。尝试通过ClassLoader.getSystemClassLoader()加载嵌套的类将失败。请注意java.util.Logging总是使用系统类加载器,由于这个原因你需要考虑一个不同的日志实现。

附录D.6. 可替代的单一jar解决方案

如果以上限制造成你不能使用Spring Boot Loader,那可以考虑以下的替代方案: